/*
 * Copyright 2013-2019 the HotswapAgent authors.
 *
 * This file is part of HotswapAgent.
 *
 * HotswapAgent is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 2 of the License, or (at your
 * option) any later version.
 *
 * HotswapAgent is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with HotswapAgent. If not, see http://www.gnu.org/licenses/.
 */
package org.hotswap.agent.plugin.spring;

import org.hotswap.agent.annotation.*;
import org.hotswap.agent.command.Scheduler;
import org.hotswap.agent.config.PluginConfiguration;
import org.hotswap.agent.config.PluginManager;
import org.hotswap.agent.javassist.*;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.plugin.spring.getbean.ProxyReplacerTransformer;
import org.hotswap.agent.plugin.spring.scanner.*;
import org.hotswap.agent.util.HaClassFileTransformer;
import org.hotswap.agent.util.HotswapTransformer;
import org.hotswap.agent.util.PluginManagerInvoker;
import org.hotswap.agent.util.classloader.ClassLoaderHelper;
import org.hotswap.agent.watch.WatchEventListenerManager;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.IllegalClassFormatException;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.*;

/**
 * Spring plugin.
 *
 * @author Jiri Bubnik
 */
@Plugin(name = "Spring", description = "Reload Spring configuration after class definition/change.", testedVersions = {"All between 3.0.1 - 5.2.2"}, expectedVersions = {"3x", "4x", "5x"}, supportClass = {ClassPathBeanDefinitionScannerTransformer.class, ProxyReplacerTransformer.class, XmlBeanDefinitionScannerTransformer.class})
public class SpringPlugin {
    private static final AgentLogger LOGGER = AgentLogger.getLogger(SpringPlugin.class);


    public static final Set<ClassLoader> EXECUTE_INIT = new HashSet<>();


    public static String[] basePackagePrefixes;

    @Init
    HotswapTransformer hotswapTransformer;

    @Init
    Scheduler scheduler;

    @Init
    ClassLoader appClassLoader;

    public void init() {
        LOGGER.info("Spring plugin initialized");
        LOGGER.info("Spring plugin initialized classloader " + appClassLoader);
        this.registerBasePackageFromConfiguration();
        this.initBasePackagePrefixes();
    }

    public void init(String version) {
        LOGGER.info("Spring plugin initialized - Spring core version '{}'", version);
        LOGGER.info("Spring plugin initialized classloader " + appClassLoader);
        this.registerBasePackageFromConfiguration();
        this.initBasePackagePrefixes();
    }

    private void initBasePackagePrefixes() {
        PluginConfiguration pluginConfiguration = new PluginConfiguration(this.appClassLoader);
        if (basePackagePrefixes == null || basePackagePrefixes.length == 0) {
            basePackagePrefixes = pluginConfiguration.getBasePackagePrefixes();
        } else {
            String[] newBasePackagePrefixes = pluginConfiguration.getBasePackagePrefixes();
            List<String> both = new ArrayList<>(basePackagePrefixes.length + newBasePackagePrefixes.length);
            Collections.addAll(both, basePackagePrefixes);
            Collections.addAll(both, newBasePackagePrefixes);
            basePackagePrefixes = both.toArray(new String[both.size()]);
        }
    }

    @OnResourceFileEvent(path = "/", filter = ".*.xml", events = {FileEvent.MODIFY})
    public void registerResourceListeners(URL url) {
        scheduler.scheduleCommand(new XmlBeanRefreshCommand(appClassLoader, url));
    }

    /**
     * register base package prefix from configuration file
     */
    public void registerBasePackageFromConfiguration() {
        if (basePackagePrefixes != null) {
            for (String basePackagePrefix : basePackagePrefixes) {
                this.registerBasePackage(basePackagePrefix);
            }
        }
    }

    private void registerBasePackage(final String basePackage) {
        final SpringChangesAnalyzer analyzer = new SpringChangesAnalyzer(appClassLoader);
        // v.d.: Force load/Initialize ClassPathBeanRefreshCommand classe in JVM. This is hack, in whatever reason sometimes new ClassPathBeanRefreshCommand()
        //       stays locked inside agent's transform() call. It looks like some bug in JVMTI or JVMTI-debugger() locks handling.

        hotswapTransformer.registerTransformer(
                appClassLoader,
                getClassNameRegExp(basePackage),
                new HaClassFileTransformer() {
                    @Override
                    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                        if (classBeingRedefined != null) {
                            if (analyzer.isReloadNeeded(classBeingRedefined, classfileBuffer)) {
                                LOGGER.info("fire ClassPathBeanRefreshCommand");
                                new ClassPathBeanRefreshCommand(classBeingRedefined.getClassLoader(), basePackage, className, classfileBuffer).executeCommand();
                            }
                        }
                        return classfileBuffer;
                    }

                    @Override
                    public boolean isForRedefinitionOnly() {
                        return true;
                    }
                });
    }

    /**
     * Register both hotswap transformer AND watcher - in case of new file the file is not known
     * to JVM and hence no hotswap is called. The file may even exist, but until is loaded by Spring
     * it will not be known by the JVM. File events are processed only if the class is not known to the
     * classloader yet.
     *
     * @param basePackage only files in a basePackage
     * @see ClassPathBeanDefinitionScannerAgent#registerBasePackage(java.lang.String)
     */
    public void registerComponentScanBasePackage(final String basePackage) {
        LOGGER.info("Registering basePackage {}", basePackage);

        this.registerBasePackage(basePackage);

        WatchEventListenerManager.addListener(
                basePackage,
                event -> {
                    LOGGER.info("fire ClassPathBeanRefreshCommand 开始");
                    String className;
                    try {
                        ByteArrayInputStream inputStream = new ByteArrayInputStream(event.getClassDefinition());
                        className = ClassPool.getDefault().makeClass(inputStream).getName();
                    } catch (IOException e) {
                        LOGGER.trace("Watch event on resource '{}' skipped, probably Ok because of delete/create event sequence (compilation not finished yet).", e, event.getPath());
                        return;
                    }
                    boolean isClassLoaded = !ClassLoaderHelper.isClassLoaded(appClassLoader, className);

                    LOGGER.info("appClassLoader:{}", appClassLoader);
                    LOGGER.info("this appClassLoader:{}", this.getClass().getClassLoader());
                    if (FileEvent.MODIFY.equals(event.getEventType())) {
                        Class<?> clazz = null;
                        try {
                            clazz = Class.forName(className, true, appClassLoader);
                        } catch (ClassNotFoundException e) {
                            LOGGER.error("加载类异常", e);
                        }
                        Map<Class<?>, byte[]> reloadMap = new HashMap<>();
                        reloadMap.put(clazz, event.getClassDefinition());
                        PluginManager.getInstance().hotswap(reloadMap);
                    }

                    if (isClassLoaded && FileEvent.CREATE.equals(event.getEventType())) {
                        ClassPool cp = new ClassPool();
                        CtClass ctClass = null;
                        try {
                            ctClass = cp.makeClass(new ByteArrayInputStream(event.getClassDefinition()));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        try {
                            Class<?> aClass = ctClass.toClass(appClassLoader, appClassLoader.getClass().getProtectionDomain());
                            LOGGER.info(aClass.getClassLoader().toString());
                        } catch (CannotCompileException e) {
                            e.printStackTrace();
                        }
                    }

                    // refresh spring only for new classes
                    LOGGER.info("fire ClassPathBeanRefreshCommand listenser");
                    new ClassPathBeanRefreshCommand(appClassLoader, basePackage, className, event).executeCommand();
                });
    }

    private String getClassNameRegExp(String basePackage) {
        String regexp = basePackage;
        while (regexp.contains("**")) {
            regexp = regexp.replace("**", ".*");
        }
        if (!regexp.endsWith(".*")) {
            regexp += ".*";
        }
        return regexp;
    }

    /**
     * Plugin initialization is after Spring has finished its startup and freezeConfiguration is called.
     * <p>
     * This will override freeze method to init plugin - plugin will be initialized and the configuration
     * remains unfrozen, so bean (re)definition may be done by the plugin.
     */
    @OnClassLoadEvent(classNameRegexp = "org.springframework.beans.factory.support.DefaultListableBeanFactory")
    public static void register(CtClass clazz) throws NotFoundException, CannotCompileException {
        LOGGER.info("register springPlugin");
        StringBuilder src = new StringBuilder("{");
        src.append("setCacheBeanMetadata(false);");
        // init a spring plugin with every appclassloader
        String initializePlugin = PluginManagerInvoker.buildInitializePlugin(SpringPlugin.class);
        LOGGER.info("register springPlugin initializePlugin:" + initializePlugin);
        src.append(initializePlugin);

        String initStrCode = PluginManagerInvoker.buildCallPluginMethod(
                SpringPlugin.class,
                "init",
                "org.springframework.core.SpringVersion.getVersion()",
                String.class.getName()
        );
        LOGGER.info("register springPlugin initStrCode:" + initStrCode);
        src.append(initStrCode);
        src.append("}");

        for (CtConstructor constructor : clazz.getDeclaredConstructors()) {
            constructor.insertBeforeBody(src.toString());
        }

        // freezeConfiguration cannot be disabled because of performance degradation
        // instead call freezeConfiguration after each bean (re)definition and clear all caches.

        // WARNING - allowRawInjectionDespiteWrapping is not safe, however without this
        //   spring was not able to resolve circular references correctly.
        //   However, the code in AbstractAutowireCapableBeanFactory.doCreateBean() in debugger always
        //   showed that exposedObject == earlySingletonReference and hence everything is Ok.
        // 				if (exposedObject == bean) {
        //                  exposedObject = earlySingletonReference;
        //   The waring is because I am not sure what is going on here.

        CtMethod method = clazz.getDeclaredMethod("freezeConfiguration");
        method.insertBefore("org.hotswap.agent.plugin.spring.ResetSpringStaticCaches.resetBeanNamesByType(this); " + "setAllowRawInjectionDespiteWrapping(true); ");
    }

    @OnClassLoadEvent(classNameRegexp = "org.springframework.aop.framework.CglibAopProxy")
    public static void cglibAopProxyDisableCache(CtClass ctClass) throws NotFoundException, CannotCompileException {
        CtMethod method = ctClass.getDeclaredMethod("createEnhancer");
        method.setBody("{"
                + "org.springframework.cglib.proxy.Enhancer enhancer = new org.springframework.cglib.proxy.Enhancer();"
                + "enhancer.setUseCache(false);"
                + "return enhancer;"
                + "}");

        LOGGER.debug("org.springframework.aop.framework.CglibAopProxy - cglib Enhancer cache disabled");
    }
}
